/******************************************************************************
 * This program is a 100% Java Email Server.
 ******************************************************************************
 * Copyright (c) 2001-2011, Eric Daugherty (http://www.ericdaugherty.com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ******************************************************************************
 * For current versions and more information, please visit:
 * http://javaemailserver.sf.net/
 *
 * or contact the authors at:
 * java@ericdaugherty.com
 * andreaskyrmegalos@hotmail.com
 *
 ******************************************************************************
 * This program is based on the CSRMail project written by Calvin Smith.
 * http://crsemail.sourceforge.net/
 ******************************************************************************
 *
 * $Rev$
 * $Date$
 *
 ******************************************************************************/

package com.ericdaugherty.mail.server.services.smtp;

//Java imports
import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.HashMap;

import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocket;
import javax.security.sasl.SaslException;

//Log imports
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

//Local imports
import com.ericdaugherty.mail.server.Mail;
import com.ericdaugherty.mail.server.auth.*;
import com.ericdaugherty.mail.server.configuration.ConfigurationManager;
import com.ericdaugherty.mail.server.configuration.RcptPolicyArray;
import com.ericdaugherty.mail.server.errors.*;
import com.ericdaugherty.mail.server.info.*;
import com.ericdaugherty.mail.server.security.transport.TransportLayer;
import com.ericdaugherty.mail.server.services.general.*;
import com.ericdaugherty.mail.server.services.smtp.support.*;


/**
 * Handles an incoming SMTP connection.  See rfc821/2821/5321 for details.
 *
 * @author Eric Daugherty
 * @author Andreas Kyrmegalos (2.x branch)
 * @author Joey Novak (bastardized catch all changes)
 */
public class SMTPProcessor extends MIMEProcessor implements ConnectionProcessor {

	private HashMap<String, String> userToErrorMap = new HashMap<String, String>();
    /** Logger Category for this class. */
    private static final Log log = LogFactory.getLog( SMTPProcessor.class );

    /** The ConfigurationManager */
    protected final ConfigurationManager configurationManager = ConfigurationManager.getInstance();

    //***************************************************************
    // Variables
    //***************************************************************

    /** Indicates if this thread should continue to run or shut down */
    private boolean running = true;

    /** The server socket used to listen for incoming connections */
    private ServerSocket serverSocket;

    /** Socket connection to the client */
    private Socket socket;

    /** The IP address of the client */
    private String clientIp;

    private String declaredHelloHostname;
    
    /** This setting defines the security state of the session.
     *  Please note the following:
     *  A connection over a standard port with the setting standardsecure=true
     *  starts as non-secure.
     *  A connection over a standard port with the setting standardsecure=false
     *  is considered secure for the duration of the session.
     *  A connection over a secure port
     *  is considered secure for the duration of the session.
     */
    private boolean isSecure;
    
    /** This setting is used to certify the encryption state of the connection */
    private boolean isEncrypted;
    
    /** This setting is used to prevent a shutdown after all DATA have been received */
    private boolean isFinishedData;
    
    /** This setting is used to track whether ESMTP was used during the last session */
    private boolean isESMTP;
    
    private boolean isRejected;

    private boolean is8bitMIMESupported;

    private boolean isHELOEnabled;
    
    /** The number of errors during a given session */
    private int errorcount;
    
    /** The maximum number of allowed errors */
    private int maxerrorcount = configurationManager.getMaxErrorCount();
    
    /** A temporary fromaddress field */
    private EmailAddress fromaddress;
    
    private int allowClearText = configurationManager.allowClearTextSMTP();
    private static final int NEVER = ConfigurationManager.NEVER;
    private static final int ALWAYS = ConfigurationManager.ALWAYS;

    private volatile boolean updatingServerSocket;
    
    protected boolean useAmavisSMTPDirectory;

    private boolean authenticated;

    private boolean forceExitRCPT;

    protected VerifyIP verifyIP;

    private StreamHandler smtpSH = new StreamHandler();

    private String[] instanceAuthMech;

    private DigestMd5ServerMode digestMd5ServerMode;
    private GSSServerMode gssServerMode;

    private TransferMode transferMode = new TransferMode();
    
    private RecipientPolicyHandler rph = new RecipientPolicyHandler();

    //***************************************************************
    // Public Interface
    //***************************************************************

    public SMTPProcessor(){
    	userToErrorMap.put("error554", REJECT_MESSAGE);
    	userToErrorMap.put("error421", FORCED_EXIT_MESSAGE);    	
    	userToErrorMap.put("error550", MESSAGE_RCPT_DOMAIN_REJECTED);
    	userToErrorMap.put("error451", MESSAGE_RCPT_DOMAIN_REJECTED);
    	userToErrorMap.put("error500", MESSAGE_INVALID_COMMAND);
    	userToErrorMap.put("error501", MESSAGE_SYNTAX_ERROR_PARAMETER);
    	userToErrorMap.put("error552", MESSAGE_MESSAGE_TOO_LARGE);
    	userToErrorMap.put("error554", MESSAGE_NO_VALID_RCPT);
    	userToErrorMap.put("error504", MESSAGE_UNRECOGNIZED_AUTH_MECH);
    	userToErrorMap.put("error503", MESSAGE_ALREADY_AUTHENTICATED);
    	userToErrorMap.put("error535", MESSAGE_AUTH_FAILED);
    	userToErrorMap.put("error503", MESSAGE_ALREADY_AUTHENTICATED);
    	userToErrorMap.put("error452", MESSAGE_TOO_MANY_RCPT);
    	userToErrorMap.put("error421", MESSAGE_EXCESS_FAIL_RCPT_DISCONNECT);
    	userToErrorMap.put("error500", "500 General Test Error");
    	userToErrorMap.put("error400", "400 General Test Error");    	    	    	
    }

    /**
     * Sets the socket used to communicate with the client.
     */
    public void setSocket( ServerSocket serverSocket ) {

        this.serverSocket = serverSocket;
        if (log.isDebugEnabled()) {
           log.debug("serverSocketUpdated");
       }
    }

    private boolean isUpdatingServerSocket() {
       synchronized(locale) {
          return updatingServerSocket;
       }
    }

    public void setUpdatingServerSocket(boolean value) {
       synchronized(locale) {
          updatingServerSocket = value;
       }
    }

    public void setUseAmavisSMTPDirectory() {
      useAmavisSMTPDirectory = false;
    }

    public void setupVerifyIP() {
       verifyIP = VerifyIPFactory.getNewVerifyIPInstance(!configurationManager.isVerifyIP());
    }

    /**
     * Entrypoint for the Thread, this method handles the interaction with
     * the client socket.
     */
    public void run() {

       if (configurationManager.isAmavisSupportActive()&&useAmavisSMTPDirectory) {
          
          transferMode.transferModeAction = new FullTransferMode();
       }
       else {

          switch(configurationManager.getTransferMode()) {
             case LOCAL:{
                transferMode.transferModeAction = new LocalTransferMode();
             }break;
             case REMOTE:{
                transferMode.transferModeAction = new RemoteTransferMode();
             }break;
             case TESTING:{
                transferMode.transferModeAction = new TestingTransferMode();
             }break;
             default:{
                transferMode.transferModeAction = new FullTransferMode();
             }
          }
       }


        boolean isconnected;
        boolean forcedexit;
        while( running ) {
           
            isconnected = false;
            forcedexit = false;
            authenticated = false;
            is8bitMIMESupported = configurationManager.is8bitMIME();
            isHELOEnabled = configurationManager.isHELOEnabled();
            isESMTP = true;
            isRejected = false;
            errorcount = 0;
            digestMd5ServerMode = new DigestMd5ServerMode(true);
            StringBuilder sb = new StringBuilder(30);
            if (configurationManager.isGSSEnabled()) {
               sb.append(AUTH_MECH[0]);
               sb.append(",");
            }
            if (configurationManager.isDigestMD5Enabled()) {
               sb.append(AUTH_MECH[1]);
               sb.append(",");
            }
            sb.append(AUTH_MECH[2]);sb.append(",");sb.append(AUTH_MECH[3]);
            instanceAuthMech = sb.toString().split(",");
            try {
                socket = serverSocket.accept();
                
                isSecure = serverSocket.getLocalPort()==configurationManager.getSecureSMTPPort()?
                      true:configurationManager.isStandardSMTPSecure()?false:true;
                isEncrypted = serverSocket.getLocalPort()==configurationManager.getSecureSMTPPort()?
                      true:false;
                isconnected = true;

                //Prepare the input and output streams.
                InetAddress remoteAddress = null;
                smtpSH.setStreams(socket);

                remoteAddress = socket.getInetAddress();
                
                clientIp = remoteAddress.getHostAddress();

                if( log.isInfoEnabled() ) { log.info( remoteAddress.getHostName() + "(" + clientIp + ") socket connected via SMTP." ); }

                //Output the welcome/reject message.
                if (!(remoteAddress.isLoopbackAddress()||remoteAddress.isSiteLocalAddress())&&verifyIP.blockIP(clientIp)) {
                   write( REJECT_MESSAGE, 0);
                   isRejected = true;
                }
                else {
                   write( WELCOME_MESSAGE , 0);
                }

                //Initialize the input message.
                resetMessage();

                //Parses the input for commands and delegates to the appropriate methods.
                forcedexit = handleCommands();

            }
            catch (TooManyErrorsException e) {
               log.error( "The session generated too many errors");
            }
            catch ( SocketTimeoutException e ) {
               if (isconnected) {
                  log.error(" Timedout waiting for client command.");
                  forcedexit = true;
               }
            }
            catch ( IOException e ) {
               if (running) {
                  log.error( " There was a error with the connection: "+e);
                  //There is a chance that an instance of this class has been instantiated but
                  //its reference in ServiceListener points to null. Check the Mail instance for
                  //the case of a server shutdown
                  if (Mail.getInstance().isShuttingDown()) {
                     shutdown();
                  }
               }
               if (socket!=null) {
                  if (!socket.isClosed()) {
                     forcedexit = true;
                  }
                  else {
                     isconnected = false;
                  }
               }
            }
            //If any exception gets to here uncaught, it means we should just disconnect.
            catch( Throwable e ) {
               log.error( "Unknown Exception:", e );
               isconnected = false;
            }
            finally {
               digestMd5ServerMode.conclude();
               if( socket != null && isconnected) {
                     try {  
                        if (!forcedexit) {
                           write( MESSAGE_DISCONNECT , 0 );
                        }
                        else {
                           if (!forceExitRCPT) {
                              write ( FORCED_EXIT_MESSAGE, 0 );
                           }
                           else {
                              write ( MESSAGE_EXCESS_FAIL_RCPT_DISCONNECT, 0);
                           }
                        }
                     }
                     catch (TooManyErrorsException ex) {}
                     catch (IOException ioe) {}
               }
               try {
                  if( socket != null ) {
                     socket.close();
                  }
               }
               catch( IOException e ) {
                  log.error( "Error disconnecting.", e );
                  //Nothing to do.
               }
               finally {
                  socket = null;
               }
               message = null;
               smtpSH = new StreamHandler();
               if (gssServerMode!=null) gssServerMode.conclude();
            }
            while (isUpdatingServerSocket()) {
               try {
                  Thread.sleep(750);
               }
               catch (InterruptedException e) {}
               if (!running) break;
            }
        }
        log.warn( "SMTPProcessor shut down gracefully" );
    }

    /**
     * Notifies this thread to stop processing and exit.
     */
    public void shutdown() {
        log.warn( "Shutting down SMTPProcessor." );
        running = false;
        if (!isFinishedData) {
           if (socket!=null) {
              try {
                 socket.close();
              } catch (IOException ex) {}
           }
        }
    }

    //***************************************************************
    // Private Interface
    //***************************************************************

   /** Message specific variables */
   private SMTPMessage message;
   private int validRCPT, failedRCPT;
   private boolean tooManyRCPT, excessRCPT;
   private boolean receivedMAILFROM;
   private boolean receivedRCPTTO;
   private boolean singleRCPT;
   private byte[] output;
   private int finalBufferSize;
   private boolean endOfMessage;

   private void resetMessage() {
      message = new SMTPMessageImpl();
      output = null;
      finalBufferSize = 32;
      mimeBody = new MIMEBody(null);
      endOfMessage = false;
      isMessage8bitMIME = false;
      receivedMAILFROM = false;
      receivedRCPTTO = false;
      singleRCPT = false;
      validRCPT = 0;
      failedRCPT = 0;
      tooManyRCPT = false;
      excessRCPT = false;
      forceExitRCPT = false;
      isFinishedData = false;
      addDataLine = log.isDebugEnabled()?new AddDataLineDebug(message, log):new AddDataLineDefault(message);
   }

    /**
     * Checks to make sure that the incoming command is not a quit.  If so,
     * the connection is terminated.
     */
    private boolean checkQuit( String command ) {

        if( command.equals( COMMAND_QUIT ) ) {
           if (log.isDebugEnabled()) {
               log.debug( "User has QUIT the session." );
           }
            return true;
        }
        return false;
    }

    /**
     * Handles all the commands related the the sending of mail.
     */
    private boolean handleCommands() throws TooManyErrorsException,
          SocketTimeoutException, SocketException, IOException {

        //Reusable Variables.
        String inputString;
        String command;
        String argument;

        int lastCommand = NONE;
        int maxPassAttempts = configurationManager.getMaxPassAttempts();
        int passAttempts = 0;

        while( true ) {

            inputString = read();

            if (forceExitRCPT) return true;

            command = parseCommand( inputString );
            argument = parseArgument( inputString );
            if (command == null) {
               write ( MESSAGE_INVALID_COMMAND, 1 );
            }
            else if (checkQuit(command)) {
               return false;
            }
            else if (isRejected) {
               write( MESSAGE_COMMAND_ORDER_INVALID, 1);
               
            }
            else if (command.equals( COMMAND_VRFY ) ) {
                write( MESSAGE_NO_VRFY_SUPPORT, 0 );
                lastCommand = VRFY;
            }
            //NOOP - Do Nothing.
            else if( command.equals( COMMAND_NOOP ) ) {
                write( MESSAGE_OK, 0 );
                lastCommand = NOOP;
            }
            //Resets the state of the server back to the initial state.
            else if( command.equals( COMMAND_RSET ) ) {
	          write( MESSAGE_OK, 0 );
                lastCommand = RSET;
                resetMessage();
            }
            else if( command.equals( COMMAND_STLS ) ) {
               boolean acceptable = false;
               TransportLayer transportLayer = new TransportLayer();
               try {
                  transportLayer.init(socket,true,true);
                  write("220 Ready to start TLS", 0);
                  transportLayer.verifyPeer(true);
                  String cipher = ((SSLSocket)transportLayer.getSocket()).getSession().getCipherSuite();
                  log.info("Negotiated Cipher: "+cipher);
                  String[] ec = configurationManager.getEnabledCiphers();
                  for (int i=0;i<ec.length;i++) {
                     if (cipher.equals(ec[i])) {
                        acceptable = true;
                        break;
                     }
                  }
                  if (!acceptable) {
                     log.info("Negotiated Cipher Suite not acceptable!");
                     continue;
                  }
                  socket = transportLayer.getSocket();
               }
               catch (SSLPeerUnverifiedException sslpue) {
                  log.error( sslpue.getMessage() );
                  write ( "454 Peer Verification Failed", 1 );
                  continue;
               }
               catch (IOException ioe) {
                  log.error( ioe.getMessage() );
                  write ( "454 TLS temporarily unavailable", 1 );
                  continue;
               }
               finally {
                  transportLayer.conclude();
               }
               isSecure = true;
               isEncrypted = true;
               try {
                  smtpSH.setSecureStreams(socket);

               } catch (IOException ex) {
                  log.error(ex.getMessage());
                  //forced exit, will write a 421 to the channel
                  return true;
               }
            }
            else if( command.equals( COMMAND_AUTH ) ) {
               if (authenticated) {
                  write( MESSAGE_ALREADY_AUTHENTICATED, 1 );
               }
               else {
                  if (maxPassAttempts!=0) {
                     passAttempts++;
                     if (passAttempts>maxPassAttempts) {
                        write( MESSAGE_AUTH_FAILED, 1 );
                        continue;
                     }
                  }
                  String[] authBreakDown = argument.split(" ");
                  String mechanism = authBreakDown[0].toUpperCase(locale);
                  String clientResponse = null;
                  if (authBreakDown.length>1) {
                     clientResponse = authBreakDown[1];
                  }
                  boolean contains = false;
                  int mechCount = instanceAuthMech.length-(allowClearText==ALWAYS?0:(allowClearText==NEVER?2:(isEncrypted?0:2)));
                  for (int i=0;i<mechCount;i++) {
                     if (instanceAuthMech[i].contains(mechanism)) {
                        contains = true;
                        break;
                     }
                  }
                  if (contains) {
                     if (mechanism.equals("PLAIN")) {
                        PlainServerMode plainServerMode = new PlainServerMode(true);
                        if (clientResponse==null) {
                           write ( MESSAGE_INTERMEDIATE, 0 );
                           clientResponse = read();
                        }
                        try {
                           if (clientResponse.equals("*")) throw new AuthenticationException("Client cancelled authentication process");
                           byte[] rspauth = plainServerMode.evaluateResponse(clientResponse.getBytes(US_ASCII));
                           if (rspauth == null) {
                              write ( MESSAGE_AUTH_FAILED, 1 );
                           }
                           else {
                              authenticated = true;
                              write ( MESSAGE_AUTH_SUCCESS+new String(rspauth,US_ASCII), 0 );
                           }
                        }
                        catch (AuthenticationException ae) {
                           log.error(ae.getMessage());
                           write ( MESSAGE_AUTH_CANCELLED+"Authentication cancelled", 1 );
                        }
                        catch (SaslException ex) {
                           log.error(ex.getMessage());
                           if (ex.getCause()!=null) {
                              if (ex.getCause() instanceof MalformedBase64ContentException) {
                                 write ( MESSAGE_AUTH_CANCELLED+ex.getMessage(), 1 );
                              }
                              else {
                                 write ( MESSAGE_AUTH_FAILED, 1 );
                              }
                           }
                           else {
                              write ( MESSAGE_AUTH_FAILED, 1 );
                           }
                        } catch (UnsupportedEncodingException ex) {
                           write ( MESSAGE_FAILED_TRANSACTION, 1 );
                        }
                        plainServerMode.conclude();
                     }
                     else if (mechanism.equals("LOGIN")) {
                        LoginServerMode loginServerMode = new LoginServerMode(true);
                        write ( MESSAGE_INTERMEDIATE + "VXNlcm5hbWU6", 0 );
                        clientResponse = read();
                        try {
                           if (clientResponse.equals("*")) throw new AuthenticationException("Client cancelled authentication process");
                           byte[] rspauth = loginServerMode.evaluateResponse(clientResponse.getBytes("UTF-8"));
                           if (rspauth != null) {
                              write ( MESSAGE_INTERMEDIATE + "UGFzc3dvcmQ6", 0 );
                              clientResponse = read();
                              if (clientResponse.equals("*")) throw new AuthenticationException("Client cancelled authentication process");
                              rspauth = loginServerMode.evaluateResponse(clientResponse.getBytes("UTF-8"));
                              if (rspauth != null) {
                                 authenticated = true;
                                 write ( MESSAGE_AUTH_SUCCESS+new String(rspauth, US_ASCII), 0 );
                              }
                           }
                           if (rspauth == null) {
                              write ( MESSAGE_AUTH_FAILED, 1 );
                           }
                        }
                        catch (AuthenticationException ae) {
                           log.error(ae.getMessage());
                           write ( MESSAGE_AUTH_CANCELLED+"Authentication cancelled", 1 );
                        }
                        catch (SaslException ex) {
                           log.error(ex.getMessage());
                           if (ex.getCause()!=null) {
                              if (ex.getCause() instanceof MalformedBase64ContentException) {
                                 write ( MESSAGE_AUTH_CANCELLED+ex.getMessage(), 1 );
                              }
                              else {
                                 write ( MESSAGE_AUTH_FAILED, 1 );
                              }
                           }
                           else {
                              write ( MESSAGE_AUTH_FAILED, 1 );
                           }
                        }
                        loginServerMode.conclude();
                     }
                     else if (mechanism.equals("DIGEST-MD5")) {
                        try {
                           write ( MESSAGE_INTERMEDIATE+new String(digestMd5ServerMode.evaluateResponse(null), US_ASCII ), 0 );
                           clientResponse = read();
                           if (clientResponse.equals("*")) throw new AuthenticationException("Client cancelled authentication process");
                           byte[] rspauth = digestMd5ServerMode.evaluateResponse(clientResponse.getBytes( US_ASCII ));
                           if (rspauth == null) {
                              write ( MESSAGE_AUTH_FAILED, 1 );
                           }
                           else {
                              authenticated = true;
                              write ( MESSAGE_INTERMEDIATE+new String(rspauth, US_ASCII), 0 );
                              read();
                              write ( MESSAGE_AUTH_SUCCESS+"Authentication successful", 0 );
                              if (digestMd5ServerMode.isProtected()) smtpSH.setSaslServer(digestMd5ServerMode);
                           }
                        }
                        catch (AuthenticationException ae) {
                           log.error(ae.getMessage());
                           write ( MESSAGE_AUTH_CANCELLED+"Authentication cancelled", 1 );
                        }
                        catch (SaslException ex) {
                           log.error(ex.getMessage());
                           if (ex.getCause()!=null) {
                              if (ex.getCause() instanceof AuthenticationException) {
                                 write ( MESSAGE_AUTH_FAILED_CUSTOM+ex.getMessage(), 1 );
                              }
                              else if (ex.getCause() instanceof MalformedBase64ContentException) {
                                 write ( MESSAGE_AUTH_CANCELLED+ex.getMessage(), 1 );
                              }
                              else {
                                 write ( MESSAGE_AUTH_FAILED, 1 );
                              }
                           }
                           else {
                              write ( MESSAGE_AUTH_FAILED, 1 );
                           }
                        }
                     }
                     else if (mechanism.equals("GSSAPI")) {
                        try {
                           write ( MESSAGE_INTERMEDIATE, 0 );
                           gssServerMode = AuthContext.getInstance().getGSSServerMode(false, null);
                           int tokenLength = 12288;
                           byte[] token = new byte[tokenLength], temp;
                           while (true) {
                              tokenLength = smtpSH.read(token, 0, tokenLength);
                              temp = new byte[tokenLength];
                              System.arraycopy(token, 0, temp, 0, tokenLength);
                              if (temp.length==1&&temp[0]==0x2a) throw new AuthenticationException("Client cancelled authentication process");
                              token = gssServerMode.evaluateResponse(temp);
                              if (gssServerMode.getGssSaslServer().isComplete()) {
                                 break;
                              }
                              write ( MESSAGE_INTERMEDIATE+new String(token,US_ASCII), 0 );
                              tokenLength = 12288;
                              token = new byte[tokenLength];
                           }
                           write ( MESSAGE_AUTH_SUCCESS+"Authentication successful", 0 );
                           if (gssServerMode.isProtected()) smtpSH.setSaslServer(gssServerMode.getGssSaslServer());

                        }
                        catch (AuthenticationException ae) {
                           log.error(ae.getMessage());
                           write ( MESSAGE_AUTH_CANCELLED+"Authentication cancelled", 1 );
                        }
                        catch (SaslException ex) {
                           log.error(ex.getMessage());
                           if (ex.getCause()!=null) {
                              if (ex.getCause() instanceof MalformedBase64ContentException) {
                                 write ( MESSAGE_AUTH_CANCELLED+ex.getMessage(), 1 );
                              }
                              else {
                                 write ( MESSAGE_AUTH_FAILED, 1 );
                              }
                           }
                           else {
                              write ( MESSAGE_AUTH_FAILED, 1 );
                           }
                        }
                     }
                  }
                  else {
                     write ( MESSAGE_UNRECOGNIZED_AUTH_MECH, 1 );
                  }
               }
            }
            else if( command.equals( COMMAND_EHLO ) ) {
               declaredHelloHostname = argument;
                write( "250-Hello " + declaredHelloHostname, 0 );
                if (!isSecure) {
                   write( "250-STARTTLS", 0 );
                }
                if (is8bitMIMESupported) {
                   write( "250-8BITMIME ", 0 );
                }
                StringBuilder auth_mech = new StringBuilder();
                int mechCount = instanceAuthMech.length-(allowClearText==ALWAYS?0:(allowClearText==NEVER?2:(isEncrypted?0:2)));
                if (mechCount>0) {
                   for (int i=0;i<mechCount;i++) {
                      auth_mech.append(" ").append(instanceAuthMech[i]);
                   }
                   write ("250-AUTH"+auth_mech.toString(), 0 );
                }
                write( "250 SIZE "+(configurationManager.getMaximumMessageSize() * 1024 * 1024), 0  );
                lastCommand = EHLO;
            }
            else if( command.equals( COMMAND_HELO ) ) {
               if (isHELOEnabled) {
                   isESMTP = false;
                   write( "250 Hello " + argument, 0 );
               }
               else {
                   write ( MESSAGE_NO_HELO_ACCEPTED, 1 );
               }
               lastCommand = HELO;
            }
            //Not only check the command, but the full string, since the prepare command
            //method only returns the text before the first string, and this is a two
            //word command.
            else if( command.equals( COMMAND_MAIL_FROM ) && inputString.toUpperCase(locale).startsWith( "MAIL FROM:" ) ) {

                if( (lastCommand == HELO && isHELOEnabled) || lastCommand == NONE || lastCommand == RSET || lastCommand == EHLO || lastCommand == VRFY || lastCommand == NOOP) {
                    if( transferMode.handleMailFrom( inputString ) ) {
                        lastCommand = MAIL_FROM;
                        receivedMAILFROM = true;
                    }
                }
                else {
                    write( MESSAGE_COMMAND_ORDER_INVALID, 1 );
                }
            }
            //Not only check the command, but the full string, since the prepare command
            //method only returns the text before the first string, and this is a two
            //word command.
            else if( command.equals( COMMAND_RCPT_TO ) && inputString.toUpperCase(locale).startsWith( "RCPT TO:" ) ) {

                if(receivedMAILFROM && ( lastCommand == MAIL_FROM || lastCommand == RCPT_TO || lastCommand == NONE || lastCommand == EHLO || lastCommand == VRFY || lastCommand == NOOP)) {
                   if (excessRCPT) {
                      write( MESSAGE_EXCESS_RCPT , 0 );
                   }
                   else if (tooManyRCPT) {
                      write( MESSAGE_TOO_MANY_RCPT , 0 );
                   }
                   else {
                	   String toAddress = parseAddress( inputString.substring( inputString.indexOf(':') ).trim() );
                       try {
                    	    EmailAddress address = new EmailAddress( toAddress );
	 						if(userToErrorMap.containsKey(address.getUsername())){
	 							write( userToErrorMap.get(address.getUsername()), 1 );
	 						} else {
	 							transferMode.handleRcptTo( inputString );
	 						}
	 				  } catch (InvalidAddressException e) {
	 						// TODO Auto-generated catch block
	 						e.printStackTrace();
	 			      }
 
                      
                      lastCommand = RCPT_TO;
                      receivedRCPTTO = true;
                      if(!singleRCPT) {
                         singleRCPT = true;
                      }
                      else {
                         singleRCPT = false;
                      }
                   }
                }
                else {
                    write( MESSAGE_COMMAND_ORDER_INVALID, 1 );
                }
            }
            else if( command.equals( COMMAND_DATA ) ) {

                if( receivedMAILFROM && receivedRCPTTO && lastCommand == RCPT_TO && message.getToAddresses().size() > 0 ) {
                    handleData();
                    lastCommand = DATA;
                    // Reset for another message
                    resetMessage();
                }
                else if (message.getToAddresses().isEmpty()) {
                   write( MESSAGE_NO_VALID_RCPT, 1 );
                }
                else {
                    write( MESSAGE_COMMAND_ORDER_INVALID, 1 );
                }
            }
            else {
               if (isFinishedData && !isESMTP && message.getSize()>configurationManager.getMaximumMessageSize() * 1024 * 1024) {
                  return !checkQuit(command);
               }
               write( MESSAGE_INVALID_COMMAND + command, 1 );
            }
        }
    }

    private class TransferMode {

       private TransferModeAction transferModeAction;

       public boolean handleMailFrom(String inputString) throws TooManyErrorsException, IOException {

          String fromAddress = parseAddress( inputString.substring( inputString.indexOf(':') ).trim() );

          try {
             return transferModeAction.handleMailFrom(fromAddress);
          }
          catch( InvalidAddressException iae ) {
             log.error( "Unable to parse From Address: " + fromAddress );
             write( MESSAGE_SYNTAX_ERROR_PARAMETER, 1 );
             return false;
          }
       }

       public void handleRcptTo(String inputString) throws TooManyErrorsException, IOException {

          String toAddress = parseAddress( inputString.substring( inputString.indexOf(':') ).trim() );

          try {
             EmailAddress address = new EmailAddress( toAddress );
             transferModeAction.handleRcptTo(address);
          }
          catch( InvalidAddressException iae ) {
             write( MESSAGE_USER_INVALID, 1 );
             failedRCPT++;
             rcptPolicyActions((EmailAddress)null);
             log.error( "RCTP TO: " + toAddress + " rejected." );
             return;
          }
       }
    }

    private abstract class TransferModeAction {

       /**
        * Handle the "MAIL FROM:" command, which defines the sending address for
        * this message. No checking for the validity of the sender's identity
        * is performed but should the domain be a registered local one then
        * the credentials are checked after the RCPT TO commands is received.
        */
       public abstract boolean handleMailFrom(String fromAddress) throws TooManyErrorsException, IOException, InvalidAddressException;

       /**
        * Handle the "RCPT TO:" command, which defines one of the receiving addresses.
        */
       public abstract void handleRcptTo(EmailAddress address) throws TooManyErrorsException, IOException, InvalidAddressException;
    }

    private class FullTransferMode extends TransferModeAction{

       public boolean handleMailFrom(String inputString) throws TooManyErrorsException, IOException, InvalidAddressException {

          handleMailFrom0(inputString);
          return handleMailFrom1(inputString);
       }

       protected void handleMailFrom0(String fromAddress) throws InvalidAddressException{

          //It is legal for the MAIL FROM address to be empty.
          if( fromAddress == null || fromAddress.trim().equals( "" ) ) {
             fromaddress = new EmailAddress();
             if (log.isDebugEnabled()) {
                log.debug( "MAIL FROM is NULL" );
             }
          }
          //Although this is the normal case...
          else {
             fromaddress = new EmailAddress( fromAddress );
             if( log.isDebugEnabled() ) { log.debug( "MAIL FROM: " + fromAddress ); }
          }
       }

       protected boolean handleMailFrom1(String inputString) throws TooManyErrorsException, IOException {

          String[] list = inputString.toUpperCase(locale).substring(inputString.lastIndexOf('>')+1).split(" ");
          for (int i=0;i<list.length;i++) {
             if (list[i].contains("SIZE")) {
                try {
                   int size = Integer.parseInt(list[i].substring(list[i].indexOf('=')+1));
                   if (size > (configurationManager.getMaximumMessageSize() * 1024 * 1024)) {
                      log.warn( "Message Rejected.  Message larger than max allowed size (" + configurationManager.getMaximumMessageSize() + " MB)" );
                      write( MESSAGE_MESSAGE_TOO_LARGE, 0 );
                      return false;
                   }
                }
                catch(NumberFormatException nfe){}
             }
             else if (is8bitMIMESupported && list[i].indexOf("8BITMIME")!=-1) {
                if (log.isDebugEnabled()) {
                   log.debug("Receiving message has 8BITMIME parts");
                }
                message.set8bitMIME(isMessage8bitMIME = true);
             }
          }
          message.setFromAddress( fromaddress );
          write( MESSAGE_OK, 0 );
          return true;
       }

       public void handleRcptTo( EmailAddress address ) throws TooManyErrorsException, IOException, InvalidAddressException {

          handleRcptTo0(address);
       }

       protected void handleRcptTo0( EmailAddress address ) throws TooManyErrorsException, IOException, InvalidAddressException{

          //Check the address to see if we can deliver it.
          DeliveryService deliveryService = DeliveryService.getDeliveryService();
          if( authenticated || deliveryService.acceptAddress( address, clientIp, message.getFromAddress() ) ) {

             // Check to see if it is a local user.
             if( configurationManager.getUser( address ) == null ) {

                if ( configurationManager.isLocalDomain( address.getDomain() ) &&
                   configurationManager.isNonExistentLocalRejected() ) {
                   throw new InvalidAddressException();
                }
             }

             if (rcptPolicyActions(address)) {

                write( MESSAGE_OK, 0 );
                validRCPT++;
                message.addToAddress( address );
                if( log.isDebugEnabled() ) { log.debug( "RCTP TO: " + address + " accepted." ); }

             }
          }
          else {
              
             write ( MESSAGE_USER_INVALID, 1 );
             failedRCPT++;
             rcptPolicyActions(address);
             if( log.isInfoEnabled() ) log.info( "Invalid delivery address for incoming mail: " + address +
                   " from client: " + clientIp + " / " + message.getFromAddress());

          }
       }
    }

    private final class LocalTransferMode extends FullTransferMode{

       public void handleRcptTo( EmailAddress address ) throws TooManyErrorsException, IOException, InvalidAddressException {

          if ( !configurationManager.isLocalDomain( address.getDomain() ) ) {
             throw new InvalidAddressException();
          }
          handleRcptTo0(address);
       }
    }

    private final class RemoteTransferMode extends FullTransferMode{

       public boolean handleMailFrom(String inputString) throws TooManyErrorsException, IOException, InvalidAddressException {

          handleMailFrom0(inputString);
          if (!configurationManager.isLocalDomain(fromaddress.getDomain())) {
             throw new InvalidAddressException();
          }
          return handleMailFrom1(inputString);
       }

       public void handleRcptTo( EmailAddress address ) throws TooManyErrorsException, IOException, InvalidAddressException {

          if ( configurationManager.isLocalDomain( address.getDomain() ) ) {
             throw new InvalidAddressException();
          }
          handleRcptTo0(address);
       }
    }

    private class TestingTransferMode extends FullTransferMode{

       public void handleRcptTo( EmailAddress address ) throws TooManyErrorsException, IOException, InvalidAddressException {

          message.addToAddress( address );
          write( MESSAGE_OK, 0 );
          validRCPT++;
          //rcptPolicyActions();
          if( log.isDebugEnabled() ) { log.debug( "RCTP TO: " + address + " accepted." ); }
       }
    }

    private boolean rcptPolicyActions(EmailAddress toAddress) throws IOException, TooManyErrorsException{
       if (validRCPT>configurationManager.getMaxValidRCPT()) {
          tooManyRCPT = true;
          if (((validRCPT+failedRCPT)*100/validRCPT)>100+configurationManager.getAddPctRCPT()) {
             excessRCPT = true;
          }
       }
       if (failedRCPT>configurationManager.getMinTotFailRCPT() && failedRCPT*100/(failedRCPT+validRCPT)>configurationManager.getMinPctFailRCPT()) {
          forceExitRCPT = true;
       }
       if (toAddress==null) return true;
       String senderDomain = message.getFromAddress().getDomain().toLowerCase(locale);
       if(!configurationManager.isLocalDomain(senderDomain)) return true;
       String rcptDomain = toAddress.getDomain().toLowerCase(locale);
       RcptPolicyArray<String> domain = configurationManager.getRcptPolicyMap().get(new Domain(senderDomain));
       RcptPolicyArray<String> global = configurationManager.getRcptPolicyMap().get("#####");
       
       boolean pass = rph.rcptPolicyActions(senderDomain, rcptDomain, domain, global);
       if (!pass) {
           write( MESSAGE_RCPT_DOMAIN_REJECTED, 1 );
           return false;
       }
       log.debug("Message from domain "+senderDomain+" to Domain "+rcptDomain+" is ACCEPTED!");
       return true;

    }

    /**
     * Accepts the data being written to the socket.
     */
    private void handleData() throws TooManyErrorsException, SocketTimeoutException,
          SocketException, IOException{
       
        // Get the current maxSize setting and convert to bytes.
        long maxSize = configurationManager.getMaximumMessageSize() * 1024 * 1024;

        write( MESSAGE_SEND_DATA, 0 );

        verifyIP.saveBegin(message, useAmavisSMTPDirectory);

        // Add a "Received:" line as trace information per rfc 2821/4.4
        addDataLine.addDataLine(("Received: from " + declaredHelloHostname + " (["+ clientIp + "])").getBytes(US_ASCII));
        if (isSecure &&
              ((configurationManager.isSecureActive() && serverSocket.getLocalPort()==configurationManager.getSecureSMTPPort()) ||
                 (configurationManager.isStandardSMTPSecure() && serverSocket.getLocalPort()==configurationManager.getSMTPPort()))){
           StringBuilder secureString = new StringBuilder("        (using ");
           secureString.append(((SSLSocket)socket).getSession().getProtocol()).append(" protocol with ");
           secureString.append(((SSLSocket)socket).getSession().getCipherSuite()).append(" ciphersuite.)");
           addDataLine.addDataLine(secureString.toString().getBytes(US_ASCII));
        }

        String domain = ((EmailAddress)message.getToAddresses().get(0)).getDomain();
        setDomain(domain);

        addDataLine.addDataLine(("        by " + domain + " with " +
              (isESMTP?"E":"")+"SMTP id <REPLACE-ID>"+(singleRCPT?"":";")).getBytes(US_ASCII) );

        addDataLine.addDataLine(((singleRCPT?"        for <REPLACE-RCPT>; ":"        ")+
              new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", locale).format(message.getTimeReceived())).getBytes(US_ASCII));
                
        readOnceInputStream();
        if (!endOfMessage) {
           for (;;) {
              readInputStream();
              if (stopIfTooBig(maxSize)) break;
              if (endOfMessage)break;
           }
        }
        isFinishedData = true;
        try {
            if (endOfMessage) {
               addDataLine.flush();
               if (!message.getSMTPPersistenceProccessor().saveFinish()) {
                  throw new Exception();
               }
               write( MESSAGE_OK, 0 );
               if( log.isInfoEnabled() )
                  log.info( "Message " + message.getSMTPUID() + " accepted for delivery." );
               
            }
            else {
               if (isESMTP || message.getSize() <= maxSize) {
                  throw new Exception();
               }
            }
        }
        catch ( Exception e ) {
            log.error( "Unable to save incoming message." );
            write( MESSAGE_SAVE_MESSAGE_ERROR, 0 );
        }
    }

    private boolean stopIfTooBig(long maxSize) throws TooManyErrorsException, IOException{
        // Check message size
        if( message.getSize() > maxSize ) {
           log.warn( "Message Rejected. Message larger than max allowed size (" + configurationManager.getMaximumMessageSize() + " MB)" );
           if (isESMTP) {
              write( MESSAGE_MESSAGE_TOO_LARGE, 0 );
           }
           return true;
        }
        return false;
    }

   private void readOnceInputStream() throws IOException{

      int currentRead = -1;
      byte[] buffer = new byte[finalBufferSize+1];
      currentRead = smtpSH.read(buffer, 0, finalBufferSize);
      if (currentRead == -1) return;
      currentRead = removeInitialNull(buffer, currentRead);
      
      constructLineOfText(currentRead, buffer);
   }

   private void readInputStream() throws IOException{

      int currentRead = -1;
      byte[] buffer = new byte[finalBufferSize+1];
      currentRead = smtpSH.read(buffer, 0, finalBufferSize);
      if (currentRead == -1) return;
      
      constructLineOfText(currentRead, buffer);
   }

   private void constructLineOfText(int currentRead, byte[] buffer) throws IOException {

      int nextByte = -1, previousRead = 0;
      for (int i=0;i<currentRead;i++) {
         if (buffer[i] == 0x0d || buffer[i] == 0x0a) {
            if (i+1 == currentRead) {
               nextByte = smtpSH.read();
               if (nextByte != -1) {
                  buffer[i+1] = (byte)nextByte;
                   i = constructLineOfText3(buffer, true, previousRead, i);
                   if (endOfMessage) return;
               }
               //Perhaps a truncated end of DATA transmission. Check if it so.
               else if (output==null && i-previousRead == 1 && buffer[previousRead]==0x2E) {
                  endOfMessage = true;
                  return;
               }
               break;
            }
            else {
               i = constructLineOfText3(buffer, false, previousRead, i);
               if (endOfMessage) return;
               previousRead = i+1;
            }
         }
         if (i==currentRead-1) {
            output = constructLineOfText2(output,buffer,previousRead,currentRead);
            finalBufferSize*=2;
         }
      }
   }

   private byte[] constructLineOfText2(byte[] output, byte[] buffer, int startSegmentCount, int currentSegmentCount) {

      if (output!=null) {
         int tempOutputLength;
         byte[] tempOutput;
         tempOutputLength = output.length;
         tempOutput = new byte[tempOutputLength];
         System.arraycopy(output, 0, tempOutput, 0, tempOutputLength);
         output = new byte[tempOutputLength+currentSegmentCount-startSegmentCount];
         System.arraycopy(tempOutput, 0, output, 0, tempOutputLength);
         System.arraycopy(buffer, startSegmentCount, output, tempOutputLength, currentSegmentCount-startSegmentCount);
      }
      else {
         output = new byte[currentSegmentCount-startSegmentCount];
         System.arraycopy(buffer, startSegmentCount, output, 0, currentSegmentCount-startSegmentCount);
      }
      return output;
   }

   private int constructLineOfText3(byte[] buffer, boolean increment, int previousRead, int i) throws IOException {
      
      if (buffer[i+1] == 0x0a) {
         i++;
         output = constructLineOfText2(output,buffer,previousRead,i-1);
         int outputLength = output.length;
         if (outputLength>16&&outputLength<=128) finalBufferSize = outputLength+2;
         if (endOfMessage = checkEndOfDATA(output)) return -1;
         processDATA(output);
         if (buffer[i-1] == 0x0a) {
            processDATA(new byte[0]);
         }
         output = null;
      }
      else {
         if (increment) i++;
         if (buffer[i] == 0x0a && output!=null && output[output.length-1] == 0x0d) {
            output = constructLineOfText2(output,output,0,output.length);
            processDATA(output);
            output = null;
         }
         else {
            output = constructLineOfText2(output,buffer,previousRead,i+1);
         }
      }
      return i;
   }

   private int removeInitialNull(byte[] input, int currentRead) {

      if (input[0]>=0x20) return currentRead;
      int length = input.length-1, count = 1;
      for (;count<length;count++) {
         if (input[count]>=0x20) break;
      }
      int newCount=count;
      for (;newCount<length;newCount++) {
         input[newCount-count] = input[newCount];
      }
      newCount = length-count;
      for (;newCount<length;newCount++) {
         input[newCount] = 0x00;
      }

      return length-count;
   }

    /**
     * Reads a line from the input stream and returns it.
     */
    public String read() throws SocketException, SocketTimeoutException, IOException{

       try {
          socket.setSoTimeout( 5 * 60 * 1000);
          String inputLine = smtpSH.readLine();
          //Log the input, unless it is a password.
          if( log.isDebugEnabled() && !inputLine.toUpperCase(locale).startsWith( "PASS" ) ) {
             log.debug( "Read Input: " + inputLine );
          }
          return inputLine;
       }
       catch (NullPointerException npe) {
          return null;
       }
    }

    /**
     * Writes the specified output message to the client.
     */
    public void write( String message, int errorIncrement) throws TooManyErrorsException, IOException {
       errorcount += errorIncrement;
       if (errorcount>=maxerrorcount) throw new TooManyErrorsException();
       if (message!=null) {
          if( log.isDebugEnabled() ) { log.debug( "Writing Output: " + message ); }
          smtpSH.print(message);
       }
    }

    /**
     * Parses the input stream for the command.  The command is the
     * begining of the input stream to the first space.  If there is
     * space found, the entire input string is returned.
     * <p>
     * This method converts the returned command to uppercase to allow
     * for easier comparison.
     * <p>
     * Additinally, this method checks to verify that the quit command
     * was not issued.  If it was, a null String is returned to terminate
     * the connection.
     */
    private String parseCommand( String inputString ) {

        if (inputString == null) return null;
        int index = inputString.indexOf( " " );
        String command = index == -1 ?
              inputString.toUpperCase():inputString.substring( 0, index ).toUpperCase();
        return command;
    }

    /**
     * Parses the input stream for the argument.  The argument is the
     * text starting afer the first space until the end of the inputstring.
     * If there is no space found, an empty string is returned.
     * <p>
     * This method does not convert the case of the argument.
     */
    private String parseArgument( String inputString ) {

        if (inputString == null) return null;
        int index = inputString.indexOf( " " );
        return index == -1 ? "":inputString.substring( index + 1 );
    }

    /**
     * Parses an address argument into a real email address.  This
     * method strips off any &gt; or &lt; symbols.
     */
    private String parseAddress( String address ) {

        int index = address.indexOf( "<" );
        if( index != -1 ) {
            address = address.substring( index + 1 );
        }
        index = address.indexOf( ">" );
        if( index != -1 ) {
            address = address.substring( 0, index );
        }
        address = address.substring(address.indexOf(':')+1);
        return address;
    }

    //***************************************************************
    // Constants
    //***************************************************************

    //Message Constants
    //General Message
    private static final String WELCOME_MESSAGE = "220 ESMTP Server";
    private static final String REJECT_MESSAGE = "554 The connecting IP has been identified as a source of unsolicited mail";
    private static final String FORCED_EXIT_MESSAGE = "421 Service not available, closing transmission channel";
    private static final String MESSAGE_DISCONNECT = "221 ESMTP server signing off";
    private static final String MESSAGE_OK = "250 OK";
    private static final String MESSAGE_COMMAND_ORDER_INVALID = "503 Bad sequence of commands";
    private static final String MESSAGE_USER_INVALID = "550 Requested action not taken: mailbox unavailable";
    private static final String MESSAGE_RCPT_DOMAIN_REJECTED = "550 Recipient Domain not accepted; a policy restriction is in place";
    private static final String MESSAGE_SEND_DATA = "354 Start mail input; end with <CRLF>.<CRLF>";
    private static final String MESSAGE_SAVE_MESSAGE_ERROR = "451 Requested action aborted: local error in processing";
    private static final String MESSAGE_INVALID_COMMAND = "500 Command Unrecognized: ";
    private static final String MESSAGE_SYNTAX_ERROR_PARAMETER = "501 Syntax error in parameters or arguments";
    private static final String MESSAGE_MESSAGE_TOO_LARGE = "552 Message size exceeds fixed maximum message size";
    private static final String MESSAGE_NO_VRFY_SUPPORT = "252 Cannot VRFY user, but will accept message and attempt delivery";
    private static final String MESSAGE_NO_VALID_RCPT = "554 no valid recipients given";
    private static final String MESSAGE_FAILED_TRANSACTION = "554 Transaction Failed. ";
    private static final String MESSAGE_NO_HELO_ACCEPTED = "554 Active Security Context rejects the HELO command";

    private static final String MESSAGE_AUTH_SUCCESS = "235 ";
    private static final String MESSAGE_INTERMEDIATE = "334 ";
    private static final String MESSAGE_ALREADY_AUTHENTICATED = "503 Already authenticated";
    private static final String MESSAGE_UNRECOGNIZED_AUTH_MECH = "504 Unrecognized authentication type";
    private static final String MESSAGE_AUTH_FAILED = "535 Authentication credentials invalid";
    private static final String MESSAGE_AUTH_CANCELLED = "501 ";
    private static final String MESSAGE_AUTH_FAILED_CUSTOM = "535 ";

    private static final String MESSAGE_TOO_MANY_RCPT = "452 Too many recipients";
    private static final String MESSAGE_EXCESS_RCPT = "503 Security policy: Excessive number of recipients";
    private static final String MESSAGE_EXCESS_FAIL_RCPT_DISCONNECT = "421 Security policy: Excessive number of failed recipients";

    //Commands
    private static final String COMMAND_EHLO = "EHLO";
    private static final String COMMAND_HELO = "HELO";
    private static final String COMMAND_RSET = "RSET";
    private static final String COMMAND_NOOP = "NOOP";
    private static final String COMMAND_QUIT = "QUIT";
    private static final String COMMAND_MAIL_FROM = "MAIL";
    private static final String COMMAND_RCPT_TO = "RCPT";
    private static final String COMMAND_DATA = "DATA";
    private static final String COMMAND_VRFY = "VRFY";
    private static final String COMMAND_STLS = "STARTTLS";
    private static final String COMMAND_AUTH = "AUTH";

    //SMTP Commands
    private static final int NONE = 0;
    private static final int EHLO = 1;
    private static final int HELO = 2;
    private static final int QUIT = 3;
    private static final int MAIL_FROM = 4;
    private static final int RCPT_TO = 5;
    private static final int DATA = 6;
    private static final int DATA_FINISHED = 7;
    private static final int RSET = 8;
    private static final int STLS = 9;
    private static final int VRFY = 10;
    private static final int NOOP = 11;
    private static final int AUTH = 12;

    private static final String[] AUTH_MECH = new String[] {"GSSAPI", "DIGEST-MD5", "LOGIN", "PLAIN"};
    
}